/*
 * Pakiet zawiera klasę obsługi macierzy
 */
package pl.kgierlicka.matrix.model;

import pl.kgierlicka.matrix.exceptions.WrongMatrixDimensionsException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Klasa reprezentująca macierz o dowolnym rozmiarze
 * @author Karolina Gierlicka
 * @version 1.0
 */
public class Matrix implements Cloneable{
    /**
     * Zawartość macierzy
     */
    private ArrayList<ArrayList<Double>> matrix;
    /**
     * Lizcba wierszy
     */
    private Integer matrixRowCount;
    /**
     * Liczba kolumn
     */
    private Integer matrixColumnCount;

    /**
     * Konstruktor klasy
     * @param matrixRowCount Liczba wierszy macierzy
     * @param matrixColumnCount Liczba kolumn macierzy
     */
    public Matrix(int rowCount, int columnCount) {
        this.matrixRowCount = rowCount;
        this.matrixColumnCount = columnCount;
        this.matrix = new ArrayList();
        
        
        for (int i=0; i<this.matrixRowCount; i++){
            ArrayList<Double> row = new ArrayList();
            for (int j=0; j<this.matrixColumnCount; j++){
                row.add(j, Double.valueOf(0));
            }
            this.matrix.add(i, row);
        }
       
    }

    /**
     * Konstruktor kopiujący
     * @param m Obiekt do skopiowania
     */
    public Matrix(Matrix m) {
        
        this(m.matrixRowCount, m.matrixColumnCount);
        
   
        for (int i=0; i < this.matrixRowCount; i++){
            for (int j=0; j < this.matrixColumnCount; j++){
                try {
                    Double d = new Double(m.getField(i, j));
                    this.setField(i, j, d);
                } catch (WrongMatrixDimensionsException ex) {
                    Logger.getLogger(Matrix.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        
    }    
    
    /**
     * Metoda zwracająca liczbę kolumn macierzy
     * @return Liczba kolumn macierzy
     */
    public Integer getMatrixColumnCount() {
        return matrixColumnCount;
    }

    /**
     * Metoda zwracająca tablicę dwuwymiarową liczb typu double (zawartość macierzy)
     * @return Zawartość macierzy
     */
    public ArrayList getMatrix() {
        return matrix;
    }

    /**
     * Metoda ustawiająca zawartość macierzy jako podaną dwuwymiarową tablicę liczb typu double
     * @param matrix Podana tablica liczb do wypełnienia macierzy
     * @throws WrongMatrixDimensionsException Wyjątek oznaczający niezgodność rozmiaru podanej tablicy z rozmiarem macierzy
     */
    public void setMatrix(double[][] m) throws WrongMatrixDimensionsException {
        if (m.length != matrixRowCount || m[0].length != matrixColumnCount){
            throw new WrongMatrixDimensionsException("Błędny rozmiar tablicy");
        }
        
      for (int i=0; i<this.matrixRowCount; i++){
        for (int j=0; j<this.matrixColumnCount; j++){
           this.setField(i, j,  m[i][j]);
        }
      }
           
    }


   /**
     * Metoda ustawiająca zawartość macierzy jako podaną dwuwymiarową tablicę liczb typu double
     * @param matrix Podana tablica liczb do wypełnienia macierzy
     * @throws WrongMatrixDimensionsException Wyjątek oznaczający niezgodność rozmiaru podanej tablicy z rozmiarem macierzy
     
    public void setMatrix(double[][] m) throws WrongMatrixDimensionsException {
        if (m.length != matrixRowCount || m[0].length != matrixColumnCount){
            throw new WrongMatrixDimensionsException("Błędny rozmiar tablicy");
        }
        
      for (int i=0; i<this.matrixRowCount; i++){
        for (int j=0; j<this.matrixColumnCount; j++){
           this.setField(i, j,  m[i][j]);
        }
      }
           
    }
*/    
    
    /**
     * Metoda zwraca liczbę wierszy macierzy
     * @return Liczba wierszy macierzy
     */
    public Integer getMatrixRowCount() {
        return matrixRowCount;
    }
    
    /**
     * Metoda ustawiająca wartość podanej pozycji macierzy
     * @param x Indeks wiersza wybranej pozycji
     * @param y Indeks kolumny wybranej pozycji
     * @param value Nowa wartość na wybranej pozycji
     * @throws WrongMatrixDimensionsException Wyjątek oznaczający podanie pozycji wykraczającej poza rozmiar macierzy
     */
    public void setField (Integer x, Integer y, Double value) throws WrongMatrixDimensionsException{
       if (x>(this.matrixRowCount-1) || x < 0){
           throw new WrongMatrixDimensionsException("setField: Błędny index wiersza: " + x + " (" + this.matrixRowCount + ")");
       }
        
       if ( y>(this.matrixColumnCount-1) || y < 0){
           throw new WrongMatrixDimensionsException("setField: Błędny index kolumny: " + y + " (" + this.matrixColumnCount + ")");
       }

       matrix.get(x).set(y, new Double(value));
        
    }
   /**
     * Metoda zwracająca wartość podanej pozycji macierzy
     * @param x Indeks wiersza wybranej pozycji
     * @param y Indeks kolumny wybranej pozycji
     * @throws WrongMatrixDimensionsException Wyjątek oznaczający podanie pozycji wykraczającej poza rozmiar macierzy
     */
    public Double getField (Integer x, Integer y) throws WrongMatrixDimensionsException{
       if (x>(this.matrixRowCount-1) || x < 0){
           throw new WrongMatrixDimensionsException("getField: Błędny index wiersza: " + x + " (" + this.matrixRowCount + ")");
       }
        
       if ( y>(this.matrixColumnCount-1) || y < 0){
           throw new WrongMatrixDimensionsException("getField: Błędny index kolumny: " + y + " (" + this.matrixColumnCount + ")");
       }

       return matrix.get(x).get(y);
        
    }

    
    @Override
   /**
     * Metoda zwracająca wartości wszystkich swoich atrybutów w postaci jednego obiektu typu String
     * @return Wartości atrybutów obiektu
     */
    public String toString() {
        String txt = "Matrix{rowCount=" + matrixRowCount + ", columnCount=" + matrixColumnCount + '}'  + "\n";
          for (ArrayList<Double> row : this.matrix) {
            for (Double value : row) {
              txt+= BigDecimal.valueOf(value).setScale(4, BigDecimal.ROUND_HALF_UP) + "   ";
            }  
            txt+= "\n";
          }
        txt+= "\n";
          
        return txt;
    }
    
    /**
     * Metoda dodająca podaną macierz do bieżącej
     * Macierze muszą mieć identyczne rozmiary
     * @param m Macierz dodawana do bieżącej
     * @return Suma bieżącej macierzy z podaną w parametrze
     * @throws WrongMatrixDimensionsException Wyjątek oznaczający niezgodność rozmiarów dodawanych macierzy
     */
    public Matrix add (Matrix m)throws WrongMatrixDimensionsException{
    if (m.matrixColumnCount!=this.matrixColumnCount || m.matrixRowCount!=this.matrixRowCount){
        throw new WrongMatrixDimensionsException("Niezgodność rozmiarów macierzy"); 
    }
    Matrix result = new Matrix(this.matrixRowCount, this.matrixColumnCount);
    for (int i=0; i<result.matrixRowCount; i++){
       for (int j=0; j<result.matrixColumnCount; j++){
           result.setField(i, j, this.getField(i, j)+ m.getField(i, j));
       }
    }
    return result;
}
 
/**
 * 
 * Metoda odejmująca podaną macierz od bieżącej
 * Macierze muszą mieć identyczne rozmiary
 * @param m Macierz odejmowana od bieżącej
 * @return Różnica macierzy bieżącej i podanej w parametrze
 * @throws WrongMatrixDimensionsException Wyjątek oznaczający niezgodność rozmiarów odejmowanych macierzy
 */
public Matrix subtract (Matrix m) throws WrongMatrixDimensionsException{
    if (m.matrixColumnCount!=this.matrixColumnCount || m.matrixRowCount!=this.matrixRowCount){
        throw new WrongMatrixDimensionsException("Niezgodność rozmiarów macierzy"); 
    }
    Matrix result = new Matrix (this.matrixRowCount, this.matrixColumnCount);
    for (int i=0; i<result.matrixRowCount; i++){
       for (int j=0; j<result.matrixColumnCount; j++){
           result.setField(i, j, this.getField(i, j) - m.getField(i, j));
       }
    }
    return result;
}

/**
 * 
 * Metoda mnożąca podaną macierz przez liczbę
 * @param a Liczba, przez którą jest mnożona bieżąca macierz
 * @return Iloczyn bieżącej macierzy przez liczbę
 */
public Matrix multiply (Double a) throws WrongMatrixDimensionsException{
     Matrix result = new Matrix (this.matrixRowCount, this.matrixColumnCount);
     for (int i=0; i<this.matrixRowCount; i++){
       for (int j=0; j<this.matrixColumnCount; j++){
           result.setField(i, j, this.getField(i, j) * a);
       }
    }
    return result;
 }  
  
 /**
  * Metoda mnożąca bieżącą macierz przez podaną w parametrze
  * @param m Macierz do wymnożenia bieżącej
  * @return Iloczyn macierzy bieżącej i podanej w parametrze
  * @throws WrongMatrixDimensionsException Wyjątek oznaczający niezgodność rozmiarów mnożonych macierzy
  */
 public Matrix multiply (Matrix m) throws WrongMatrixDimensionsException{
    if (this.matrixColumnCount!=m.matrixRowCount){
        throw new WrongMatrixDimensionsException("Niezgodność rozmiarów mnożonych macierzy"); 
    }
    Matrix result = new Matrix (this.matrixRowCount, m.matrixColumnCount);
    for (int i=0; i<this.matrixRowCount; i++) {
        for(int j=0; j<m.matrixColumnCount; j++){
            result.setField(i, j, Double.valueOf(0));
            for (int k=0; k<this.matrixColumnCount; k++){
                result.setField(i, j, result.getField(i,j) + this.getField(i, k) * m.getField(k, j));
            }
        }
       
    }
 return result;
 }
 
 
  /**
  * Metoda dzieląca podany wiersz macierzy przez liczbę
  * Metoda pomocnicza do wyznaczania macierzy odwrotnej
  * @param rowIndex Indeks wiersza, który ma być podzielony przez liczbę
  * @param a Liczba, przez którą ma być podzielony podany wiersz
  */
 private void divideRowByNumber (Integer rowIndex, Double a) throws WrongMatrixDimensionsException{
     for (int i=0; i<this.matrixColumnCount; i++){
         this.setField(rowIndex, i, this.getField(rowIndex, i)/a);         
     }    
 }

 
 
  /**
  * Metoda odejmująca od podanego wiersza macierzy inny wiersz pomnożony przez liczbę
  * Metoda pomocnicza do wyznaczania macierzy odwrotnej
  * @param minuend Indeks wiersza, od którego ma być odjęty inny
  * @param subtrahend Indeks wiersza, który ma być odjęcy 
  * @param a Liczba, przez którą ma być pomnożony podany wiersz przed odjęciem
  */
 private void subtractMultipliedRowFromRow (Integer minuend, Integer subtrahend, Double a) throws WrongMatrixDimensionsException{
     for (int i=0; i<this.matrixColumnCount; i++){
                 this.setField(minuend, i, this.getField(minuend, i)-this.getField(subtrahend, i) * a);
     }
 }
 
 /**
  * Metoda statyczna zwracająca obiekt macierzy reprezentujący macierz jednostkową o podanym rozmiarze
  * @param size Rozmiar macierzy jednostkowej
  * @return Obiekt macierzy jednostkowej o podanym rozmiarze
  * @throws WrongMatrixDimensionsException Wyjątek oznaczający błąd logiczny funkcji
  */
 public static Matrix identity (Integer size)throws WrongMatrixDimensionsException{
     Matrix result = new Matrix(size, size);
     for (int i=0; i<size; i++){
         for (int j=0; j<size; j++){
             if (i==j){
                 result.setField (i, j, Double.valueOf(1));
             }
             else{
                 result.setField (i, j, Double.valueOf(0));
             }
         }
     }
     return result;
 }
 
 /**
  * Metoda zwracająca macierz odwrotną do bieżącej
  * @return Obiekt macierzy odwrotnej do bieżącej
  * @throws WrongMatrixDimensionsException Wyjątek oznaczający błąd odwracania macierzy
  */
 public Matrix invert () throws WrongMatrixDimensionsException{
     if (this.matrixColumnCount!=this.matrixRowCount){
       throw new WrongMatrixDimensionsException ("Nie można utworzyć macierzy odwrotnej! Macierz wejściowa musi być kwadratowa");
     }
     
     Matrix tempMatrix = new Matrix(this);
     Matrix result = Matrix.identity(matrixRowCount);
     Double divider;

     for (int i=0; i<tempMatrix.matrixRowCount; i++){
         divider = tempMatrix.matrix.get(i).get(i);
         if (divider == 0) {
           throw new WrongMatrixDimensionsException("Macierz jest nieodwracalna"); 
         }
         
         tempMatrix.divideRowByNumber(i, divider);
         result.divideRowByNumber(i, divider);
         
         for (int j=0; j<tempMatrix.matrixRowCount; j++){
             
             if (i!=j){   
               divider = tempMatrix.matrix.get(i).get(i);  
               tempMatrix.subtractMultipliedRowFromRow(j, i, divider );  
               result.subtractMultipliedRowFromRow(j, i, divider);
             }             
         }
     }  
     
    return result;  
     
 }
 
  
 /**
  * Metoda dzieląca bieżącą macierz przez podaną w parametrze
  * @param m Macierz jako dzielnik do podzielenia bieżącej
  * @return Iloraz macierzy bieżącej i podanej w parametrze
  * @throws WrongMatrixDimensionsException Wyjątek oznaczający niezgodność rozmiarów dzielonych macierzy
  */
public Matrix divide (Matrix m)throws WrongMatrixDimensionsException{
     Matrix invertedM = m.invert();
    return this.multiply(invertedM);
 }

/**
  * Metoda porównująca obiekty
  * @param o Obiekt do porównania
  * @return Czy podany obiekt jest równy bieżącemu
  */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(this.getClass().equals(o.getClass()))) {
            return false;
        }
        Matrix m = (Matrix) o;

        if (this.getMatrixColumnCount() != m.getMatrixColumnCount()) {
            return false;
        }

        if (this.getMatrixRowCount() != m.getMatrixRowCount()) {
            return false;
        }

        for (int i = 0; i < this.matrixRowCount; i++) {
            for (int j = 0; j < this.matrixColumnCount; j++) {
                try {
                    if (this.getField(i, j).doubleValue() != m.getField(i, j).doubleValue()) {
                        return false;
                    }
                } catch (WrongMatrixDimensionsException ex) {
                    Logger.getLogger(Matrix.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 79 * hash + Objects.hashCode(this.matrix);
        hash = 79 * hash + Objects.hashCode(this.matrixRowCount);
        hash = 79 * hash + Objects.hashCode(this.matrixColumnCount);
        return hash;
    }
}
